/*
 * 版权所有 (C) 2015 知启蒙(ZHIQIM) 保留所有权利。[遇见知启蒙，邂逅框架梦，本文采用木兰宽松许可证第2版]
 * 
 * https://zhiqim.org/project/zhiqim_components/zhiqim_upload_large.htm
 *
 * Zhiqim UploadLarge is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *          http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.zhiqim.uploadlarge;

import java.io.File;
import java.io.RandomAccessFile;

import org.zhiqim.httpd.HttpRequest;
import org.zhiqim.kernel.extend.HashMapSS;
import org.zhiqim.kernel.util.Files;
import org.zhiqim.kernel.util.Ids;
import org.zhiqim.kernel.util.Sqls;
import org.zhiqim.kernel.util.Strings;
import org.zhiqim.kernel.util.Validates;
import org.zhiqim.orm.ORM;
import org.zhiqim.orm.dbo.Selector;
import org.zhiqim.orm.dbo.Updater;
import org.zhiqim.uploadlarge.dbo.UpllChunk;
import org.zhiqim.uploadlarge.dbo.UpllFile;

/**
 * 大文件上传处理器接口
 *
 * @version v1.0.0 @author zouzhigang 2015-10-23 新建与整理
 */
public class ZulUploader implements ZulConstants
{
    /**
     * 通过文件编号读取文件路径
     * 
     * @param fileId        文件编号
     * @return              上传文件
     * @throws Exception    可能的异常
     */
    public static UpllFile getFile(String fileId) throws Exception
    {
        return ORM.table().item(UpllFile.class, fileId);
    }
    
    /**
     * 通过文件MD5码查询是否有已上传的文件
     * 
     * @param fileMd5       文件MD5码 
     * @return              上传文件
     * @throws Exception    可能的异常
     */
    public static UpllFile queryFile(String fileMd5) throws Exception
    {
        return ORM.table().item(UpllFile.class, new Selector("fileMd5", fileMd5));
    }
    
    /**
     * 通过文件MD5码和碎片大小查询是否有正在上传的文件
     * 
     * @param fileMd5       文件MD5码 
     * @param chunkSize     碎片大小
     * @return              碎片编号，存在则返回chunkNo,不存在返回-1
     * @throws Exception    可能的异常
     */
    public static UpllChunk queryChunk(String fileMd5, int chunkSize) throws Exception
    {
        return ORM.table().item(UpllChunk.class, fileMd5, chunkSize);
    }
    
    /**
     * 保存碎片数据
     * 
     * @param data          碎片数据，第一个或中间的碎片
     * @param fileDir       文件目录，包含根目录
     * @param fileName      文件名，这里可能有中文名
     * @param fileMd5       文件MD5码
     * @param fileLength    文件总长度
     * @param chunkSize     碎片大小
     * @param chunkNum      碎片总数
     * @param chunkNo       碎片编号
     * @throws Exception    可能的异常
     */
    public static UpllChunk createChunkData(byte[] data, String fileDir, String fileName, String fileMd5, long fileLength, int chunkSize, int chunkNum) throws Exception
    {
        String fileTempPath = getFileChunkPath(fileDir, fileMd5);
        fileName = Files.getFileName(fileName);
        String fileExt = Files.getFileExt(fileName);
        
        //写入到文件
        RandomAccessFile file = new RandomAccessFile(new File(fileTempPath), _RW_);
        file.setLength(fileLength);
        file.write(data);
        file.close();
        
        //插入到数据表
        UpllChunk chunk = new UpllChunk();
        chunk.setChunkNo(1);
        chunk.setChunkNum(chunkNum);
        chunk.setChunkSize(chunkSize);
        chunk.setChunkTime(Sqls.nowTimestamp());
        chunk.setFileExt(fileExt);
        chunk.setFileMd5(fileMd5);
        chunk.setFileName(fileName);
        chunk.setFileTempPath(fileTempPath);

        ORM.table().replace(chunk);
        
        return chunk;
    }
    
    /**
     * 保存碎片数据
     * 
     * @param data          碎片数据，第一个或中间的碎片
     * @param chunk         碎片对象
     * @param chunkNo       碎片编号
     * @throws Exception    可能的异常
     */
    public static void saveChunkData(byte[] data, UpllChunk chunk, int chunkNo) throws Exception
    {
        String fileTempPath = chunk.getFileTempPath();
        
        //跳转到碎片位置，写入到文件
        RandomAccessFile file = new RandomAccessFile(new File(fileTempPath), _RW_);
        file.seek((chunkNo-1)*chunk.getChunkSize());
        file.write(data);
        file.close();
        
        //更新到数据库
        Updater updater = new Updater();
        updater.addField("chunkNo", chunkNo);
        updater.addField("chunkTime", Sqls.nowTimestamp());
        updater.addMust("fileMd5", chunk.getFileMd5());
        updater.addMust("chunkSize", chunk.getChunkSize());
        
        ORM.table().update(UpllChunk.class, updater);
    }
    
    /**
     * 清理碎片数据
     * 
     * @param fileMd5       文件MD5码
     * @param chunkSize     碎片大小
     * @throws Exception    可能的异常
     */
    public static void clearChunkData(String fileMd5, int chunkSize) throws Exception
    {
        ORM.table().delete(UpllChunk.class, fileMd5, chunkSize);
    }
    
    /********************************************************************************************/
    //三种保存文件，1、从碎片最后一个保存，2、秒传共享，3、秒传拷贝文件
    /********************************************************************************************/
    
    /**
     * 保存文件，正常从最后一个碎片中读取
     * 
     * @param chunk         文件分块
     * @param uploadRootDir 文件绝对目录
     * @param fileLength    文件长度
     * @param contentType   内容类型
     * @return              上传文件
     * @throws Exception    可能的异常
     */
    public static UpllFile saveFile(UpllChunk chunk, String uploadRootDir, long fileLength, String contentType) throws Exception
    {
        //1.最后一个碎片后重命名文件
        String fileTempPath = chunk.getFileTempPath();
        String fileDir = Files.getFileDir(fileTempPath);
        String fileCanonicalPath = getFileAvailablePath(fileDir, chunk.getFileName());
        Files.renameFile(fileTempPath, fileCanonicalPath);
        
        //2.插入文件表
        String fileId = Ids.uuid();
        String fileName = Files.getFileName(fileCanonicalPath);
        String fileExt = chunk.getFileExt();
        String fileRelativePath = Strings.removeStartsWith(fileCanonicalPath, uploadRootDir);
        
        UpllFile file = new UpllFile();
        file.setFileId(fileId);
        file.setFileName(fileName);
        file.setFileLength(fileLength);
        file.setFileRelativePath(fileRelativePath);
        file.setFileExt(chunk.getFileExt());
        file.setFileMd5(chunk.getFileMd5());
        file.setFileUrl(_PATH_UPLOAD_LARGE_PREFIX_ + fileId + Files.fixFileExt(fileExt));
        file.setFileTime(Sqls.nowTimestamp());
        file.setContentType(contentType);
        
        ORM.table().replace(file, new HashMapSS());
        
        //3.清理碎片表
        clearChunkData(chunk.getFileMd5(), chunk.getChunkSize());
        
        //4.返回结果
        return file;
    }
    
    /**
     * 秒传，从另一个文件拷贝属性过来，共享同一个路径
     * 
     * @param oFile         原始文件对象
     * @return              上传文件
     * @throws Exception    可能的异常
     */
    public static UpllFile secondSaveFile(UpllFile oFile) throws Exception
    {//秒传
        String fileId = Ids.uuid();
        String fileExt = oFile.getFileExt();
        
        UpllFile file = new UpllFile();
        file.setFileId(fileId);
        file.setFileExt(fileExt);
        file.setFileName(oFile.getFileName());
        file.setFileLength(oFile.getFileLength());
        file.setFileMd5(oFile.getFileMd5());
        file.setFileRelativePath(oFile.getFileRelativePath());
        file.setFileUrl(_PATH_UPLOAD_LARGE_PREFIX_ + fileId + Files.fixFileExt(fileExt));
        file.setFileTime(Sqls.nowTimestamp());
        file.setContentType(oFile.getContentType());
        
        ORM.table().replace(file);
        return file;
    }
    
    /**
     * 秒传&拷贝，拷贝一个副本
     * 
     * @param oFile         原始文件对象
     * @return              上传文件
     * @throws Exception    可能的异常
     */
    public static UpllFile secondCopyFile(HttpRequest request, UpllFile oFile, String fileDir, String fileName) throws Exception
    {//秒传拷贝文件
        String fileId = Ids.uuid();
        String fileCanonicalPath = getFileAvailablePath(fileDir, fileName);
        String fileExt = oFile.getFileExt();
        
        //拷贝文件
        String oFileCanonicalPath = Zuls.getFilePath(request, oFile);
        Files.copyFile(oFileCanonicalPath, fileCanonicalPath);
        
        String uploadRootDir = Zuls.getUploadRootDir(request);
        String fileRelativePath = Strings.removeStartsWith(fileCanonicalPath, uploadRootDir);
        
        //保存到数据库
        UpllFile file = new UpllFile();
        file.setFileId(fileId);
        file.setFileName(fileName);
        file.setFileExt(fileExt);
        file.setFileLength(oFile.getFileLength());
        file.setFileMd5(oFile.getFileMd5());
        file.setFileRelativePath(fileRelativePath);
        file.setFileUrl(_PATH_UPLOAD_LARGE_PREFIX_ + fileId + Files.fixFileExt(fileExt));
        file.setFileTime(Sqls.nowTimestamp());
        file.setContentType(oFile.getContentType());
        
        ORM.table().replace(file, new HashMapSS());
        
        return file;
    }
    
    /********************************************************************************************/
    //内部方法
    /********************************************************************************************/

    /** 获取一个碎片的文件名称，格式zhiqim_${fileMd5}.upll */
    private static String getFileChunkPath(String fileDir, String fileMd5)
    {
        return new StringBuilder(fileDir)
            .append(_CHUNK_FILE_NAME_PREFIX_).append(fileMd5).append(_CHUNK_FILE_NAME_EXT_)
            .toString();
    }
    
    /** 获取一个可用的文件名称，有重名的，后面加(n) */
    private static String getFileAvailablePath(String fileDir, String fileName)
    {
        String fileExt = Files.getFileExt(fileName);
        if (!Validates.isEmpty(fileExt))
        {
            fileExt = "." + fileExt;
            fileName = Strings.trimRight(fileName, fileExt);
        }
        
        String filePath = fileDir + fileName + fileExt;
        int i = 1;
        while (true)
        {//判断相同文件名
            File file = new File(filePath);
            if (!file.exists())
                return filePath;
            
            filePath = fileDir + fileName + "("+ i +")" + fileExt;
            i++;
        }
    }
}
